﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Media;
using Microsoft.VisualStudio.Language.StandardClassification;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

//http://blog.280z28.org/archives/2009/11/74/

//consider using http://buildstarted.com/2010/09/07/razor-parser-engine-for-the-razor-syntax-highlighter/

namespace T4EditorClassifier
{

  
    /// <summary>
    /// Classifier that classifies all text as an instance of the OrinaryClassifierType
    /// </summary>
    internal class T4EditorCommentClassifier : IClassifier
    {
        IClassificationType _classificationType;

        internal T4EditorCommentClassifier(IClassificationTypeRegistryService registry)
        {
            _classificationType = registry.GetClassificationType("T4EditorCommentClassifier");
            Debug.Assert(_classificationType != null);
        }

        /// <summary>
        /// This method scans the given SnapshotSpan for potential matches for this classification.
        /// In this instance, it classifies everything and returns each span as a new ClassificationSpan.
        /// </summary>
        /// <param name="trackingSpan">The span currently being classified</param>
        /// <returns>A list of ClassificationSpans that represent spans identified to be of this classification</returns>
        public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
        {
            var openers = new[] { "//", "/*" };

            if (span.IsEmpty)
                return Enumerable.Empty<ClassificationSpan>().ToList();
            var txt = span.GetText();
            var firstMatch = Regex.Match(txt, "(" + openers.Aggregate((s1, s2) => s1 + "|" + s2).Replace("*", "\\*") + "|\\*/)");
            if (firstMatch.Success == false)
                return Enumerable.Empty<ClassificationSpan>().ToList();
            //create a list to hold the results
            List<ClassificationSpan> classifications = new List<ClassificationSpan>();
            var start = firstMatch.Index;
            while (start >= 0)
            {
                SnapshotSpan openSpan;
                if (txt.Substring(start, 2) == "//")
                {
                    var cSpan = new Span(span.Start.Position + start, span.Length - start);

                    classifications.Add(new ClassificationSpan(new SnapshotSpan(span.Snapshot, cSpan), _classificationType));

                }
                else
                    if (txt.Substring(start, 2) == "*/")
                    {
                        var openSearchStart = span.Start.Add(start - 2);
                        if (FindMatchingOpenChar(openSearchStart, this, "/*", "*/", int.MaxValue, out openSpan))
                        {
                            var cSpan = new Span(openSpan.Start.Position, span.Start.Position - openSpan.Start.Position + start+2);
                            var snapSpan = new SnapshotSpan(span.Snapshot, cSpan);
                            classifications.Add(new ClassificationSpan(snapSpan, _classificationType));

                            //add the close
                            //classifications.Add(new ClassificationSpan(new SnapshotSpan(span.Snapshot, new Span(span.Start + start, 2)), _classificationType));

                        }
                        else
                        {
                            Debug.Assert(false);
                        }
                        start += 2;
                    }
                    else if (txt.Substring(start, 2) == "/*")
                    {
                        SnapshotSpan closeSpan;

                        var open = new Span(span.Start + start, 2);
                        openSpan = new SnapshotSpan(span.Snapshot, open);
                        


                        var closeSearchStart = span.Start.Add(start + 2);
                        var hasClose = FindMatchingCloseChar(closeSearchStart, this, "/*", "*/", int.MaxValue, out closeSpan);
                        if (hasClose == false)
                        {
                            classifications.Add(new ClassificationSpan(open.Start==span.Start?span:new SnapshotSpan(span.Snapshot,open.Start,span.Length), _classificationType));
                            break;
                        }
                        var cSpan = new Span(openSpan.Start.Position, closeSpan.End.Position - openSpan.Start.Position);
                        
                        classifications.Add(new ClassificationSpan(new SnapshotSpan(span.Snapshot,cSpan),
                                                                       _classificationType));
                        if (closeSpan.Start > span.Start + span.Length)
                            break;
                        start = closeSpan.Start - span.Start;
                    }
                if (txt.Length < start + 2)
                    break;
                var nextMatch = Regex.Match(txt.Substring(start+2), "(" + openers.Aggregate((s1, s2) => s1 + "|" + s2).Replace("*", "\\*") + "|\\*/)");
                if (nextMatch.Success == false)
                    break;
                Debug.Assert(nextMatch.Index+start+2 > start);
                start = nextMatch.Index+start+2;
            }

            return classifications;
        }

        //private static bool IsInCommentOrLiteral(IClassifier aggregator, SnapshotPoint point, PositionAffinity affinity)
        //{
        //    Contract.Requires(aggregator != null);
 
        //    // TODO: handle affinity
        //    SnapshotSpan span = new SnapshotSpan(point, 1);
 
        //    var classifications = aggregator.GetClassificationSpans(span);
        //    var relevant = classifications.FirstOrDefault(classificationSpan => classificationSpan.Span.Contains(point));
        //    if (relevant == null || relevant.ClassificationType == null)
        //        return false;
 
        //    return relevant.ClassificationType.IsOfType(PredefinedClassificationTypeNames.Comment)
        //        || relevant.ClassificationType.IsOfType(PredefinedClassificationTypeNames.Literal);
        //}


        internal static bool FindMatchingOpenChar(SnapshotPoint start, IClassifier aggregator, string open, string close, int maxLines, out SnapshotSpan pairSpan)
        {
            pairSpan = new SnapshotSpan(start, start);
            ITextSnapshotLine line = start.GetContainingLine();
            int lineNumber = line.LineNumber;
            int currentLineOffset = start - line.Start-1;

            // if the offset is negative, move to the previous line
            if (currentLineOffset < 0)
            {
                lineNumber--;
                line = line.Snapshot.GetLineFromLineNumber(lineNumber);
                currentLineOffset = line.Length;
            }

            string lineText = line.GetText();

            int stopLineNumber = 0;
            if (maxLines > 0 && maxLines != int.MaxValue)
                stopLineNumber = Math.Max(stopLineNumber, lineNumber - maxLines);
            Debug.Assert(currentLineOffset<1 || lineText.Length >= currentLineOffset);
            while (true)
            {
                while (currentLineOffset >= open.Length)
                {
                    Debug.Assert(lineText.Length >= currentLineOffset);
                    var current = lineText.Substring(0, currentLineOffset);
                    //char currentChar = lineText[offset];
                    // TODO: is this the correct affinity

                    if (current.EndsWith(open))
                    {

                        pairSpan = new SnapshotSpan(line.Start + currentLineOffset - open.Length, 2);
                        Debug.Assert(pairSpan.GetText() == "/*");
                        return true;

                    }
                    // TODO: is this the correct affinity
                    else if (current.EndsWith(close))
                    {
                        return false; // can't nest close tags
                    }

                    currentLineOffset--;
                }

                // move to the previous line
                lineNumber--;
                if (lineNumber < stopLineNumber)
                    break;

                line = line.Snapshot.GetLineFromLineNumber(lineNumber);
                lineText = line.GetText();
                currentLineOffset = lineText.Length;
            }

            return false;
        }

        internal static bool FindMatchingCloseChar(SnapshotPoint start, IClassifier aggregator, string open, string close, int maxLines, out SnapshotSpan pairSpan)
        {
            pairSpan = new SnapshotSpan(start.Snapshot, 1, 1);
            ITextSnapshotLine line = start.GetContainingLine();
            string lineText = line.GetText();
            int lineNumber = line.LineNumber;
            int offset = start.Position - line.Start.Position + 1;
            int stopLineNumber = start.Snapshot.LineCount - 1;
            if (maxLines < 0 || maxLines == int.MaxValue)
                stopLineNumber = int.MaxValue;
            else
                stopLineNumber = Math.Min(stopLineNumber, lineNumber + maxLines);

            while (true)
            {
                while (offset + close.Length <= line.Length)
                {
                    var openLength = open != null ? open.Length : 0;
                    //char currentChar = lineText[offset];
                    var maxsubLength = Math.Min(lineText.Length - offset, Math.Max(openLength, close.Length));
                    var current = lineText.Substring(offset, maxsubLength);
                    // TODO: is this the correct affinity
                    if (current.StartsWith(close))
                    {

                        pairSpan = new SnapshotSpan(start.Snapshot, line.Start + offset, close.Length);
                        return true;

                    }

                    offset++;
                }

                // move on to the next line
                lineNumber++;
                if (lineNumber > stopLineNumber || lineNumber >= line.Snapshot.LineCount)
                    break;

                line = line.Snapshot.GetLineFromLineNumber(lineNumber);
                lineText = line.GetText();
                offset = 0;
            }

            return false;
        }
#pragma warning disable 67
        // This event gets raised if a non-text change would affect the classification in some way,
        // for example typing /* would cause the classification to change in C# without directly
        // affecting the span.
        public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;
#pragma warning restore 67
    }
   
}
